export interface FieldTypeValueMap {
    u8: number;
    u16: number;
    u32: number;
    i8: number;
    i16: number;
    i32: number;
    f32: number;
    f64: number;
    buffer: number[];
}

export type FieldType = keyof FieldTypeValueMap;

export interface FieldOption {
    type: FieldType;
    desc?: string | undefined;
    default?: number | undefined;
}

export type BufOption = Record<string, FieldOption>;

const FIELD_TYPE_SIZE = {
    u8: 1,
    u16: 2,
    u32: 4,
    i8: 1,
    i16: 2,
    i32: 4,
    f32: 4,
    f64: 8,
    buffer: 0,
} as const satisfies Record<FieldType, number>;

const TO_LE_ARRAY = {
    u8: (v: number) => [v],
    u16: (v: number) => [v & 0xff, (v >> 8) & 0xff],
    u32: (v: number) => [v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff, (v >> 24) & 0xff],
    i8: (v: number) => [v],
    i16: (v: number) => [v & 0xff, (v >> 8) & 0xff],
    i32: (v: number) => [v & 0xff, (v >> 8) & 0xff, (v >> 16) & 0xff, (v >> 24) & 0xff],
    f32: (v: number) => new Uint8Array(new Float32Array([v]).buffer),
    f64: (v: number) => new Uint8Array(new Float64Array([v]).buffer),
    buffer: (v: number) => [v],
} as const satisfies Record<FieldType, (v: number) => number[] | Uint8Array>;

interface FieldInfo {
    readonly offset: number;
    readonly type: FieldType;
    readonly desc?: string | undefined;
}

function fieldRange(field: FieldInfo): string {
    const start = field.offset.toString(10).padStart(3, "0");
    const end = (field.offset + FIELD_TYPE_SIZE[field.type]).toString(10).padStart(3, "0");
    return `[${start}..${end}]`;
}

export class Buf<TOption extends BufOption> {
    readonly headerSize: number;

    // #growthSize: number;

    /** 有效数据 */
    #buffer: Uint8Array;

    /** 所有的数据 */
    #data: Uint8Array;

    #growth: boolean;

    readonly fields: Record<keyof TOption, FieldInfo>;

    constructor(option: TOption) {
        this.fields = Object.create(null);
        this.#growth = false;
        // this.#growthSize = 0;

        let len = 0;
        for (let [name, field] of Object.entries(option)) {
            if (this.#growth) {
                throw new Error("Buffer field must be the last field");
            }
            if (field.type === "buffer") {
                this.#growth = true;
            }

            this.fields[name as keyof TOption] = {
                offset: len,
                type: field.type,
                desc: field.desc,
            };
            len += FIELD_TYPE_SIZE[field.type];
        }

        this.headerSize = len;
        this.#buffer = new Uint8Array(len);
        this.#data = new Uint8Array(this.#buffer.buffer, 0, len);

        for (let [name, field] of Object.entries(option)) {
            if (field.default !== undefined) {
                this.set(name as keyof TOption, field.default as any);
            }
        }
    }

    get data() {
        return this.#data;
    }

    #setFix(offset: number, values: number[] | Uint8Array) {
        for (let i = 0; i < values.length; i++) {
            this.#buffer[offset + i] = values[i]!;
        }
        return true;
    }

    set<N extends keyof TOption>(name: N, value: FieldTypeValueMap[TOption[N]["type"]]): void {
        const field = this.fields[name];
        if (!field) {
            return;
        }
        if (typeof value === "number") {
            this.#setFix(field.offset, TO_LE_ARRAY[field.type](value));
        } else {
            throw new Error("Not implemented");
        }
    }

    #getValue(field: FieldInfo): string {
        let s = "";
        for (let i = 0; i < FIELD_TYPE_SIZE[field.type]; i++) {
            s += this.#buffer[field.offset + i]!.toString(16).padStart(2, "0") + " ";
        }
        return s;
    }

    toString(): string {
        let str = `length: ${this.headerSize}\n`;
        for (let n of this.#data) {
            str += n.toString(16).padStart(2, "0") + " ";
        }
        str += "\n\n";
        for (let [name, field] of Object.entries(this.fields)) {
            const values = this.#getValue(field);
            str += `${fieldRange(field)} ${values} // ${name} ${field.type}\n`;
        }
        return str;
    }
}
